手写vue(二)响应式实现

您所在的位置:网站首页 vue 事件实现原理 手写vue(二)响应式实现

手写vue(二)响应式实现

2023-03-22 05:26| 来源: 网络整理| 查看: 265

名词解释:

vm:指Vue实例

一、目标效果 vue定义

(1)新建vm时,可以通过一个data对象,或者data函数,其属性可以通过vm直接访问,而data对象可以通过vm._data获取

(2)修改vm._data.xxx时,等价于修改this.xxx

响应式对象

(1)能够监听到data对象属性的修改

(2)能够监听到data下对象属性的属性

响应式数组

(1)能够监听到通过数组方法修改数组

(2)数组中保存的对象,也能被监测修改

(3)不监听数组通过下标修改

测试先行:

var vm = new Vue({ data() { return { student: { name: 'Chicken', age: 17 }, value: 12, hobby: ['Sing', 'Jump', 'Rap', { play: ['basketball'] }] } }, }) console.log(vm) console.log('vm.value === vm._data.value:',vm.value === vm._data.value); console.log('1、修改data下属性,可触发响应式:'); vm.value = 15 console.log('2、修改data内对象属性,可触发响应式:'); vm.student.name = 'lisi' console.log('3、使用数组方法,可触发响应式:'); vm.hobby.push('football') console.log('4、直接通过数组下标修改数组,不触发响应式:'); vm.hobby[3] = 'football'

二、Vue实例定义

参考Vue源码,使用构造函数的方式,通过创建传入一个配置对象生成实例

option传给vue之后,直接通过this.$option挂载到示例上,方便后面,读取配置项进行初始化

初始化单独在一个init文件中进行

import init from "./init" function Vue(option) { // 挂载到vms上 this.$option = option init(this) }; export default Vue 三、在Vue实例中定义data属性

data中的值,可以通过多个地方修改,但修改的结果时同步的,此时就不能直接粗暴地挂载在vm上:例如:

要把data.foo挂载到this.foo,如果直接使用this.foo = data.foo,则执行this.foo = newVal之后,data.foo还是旧的值。

由上面的反例可得,我们要挂载的内容不是foo的值,而是data的foo这个属性,通过this.foo修改值,应该同步到data.foo上去

通过Object.defineProperty可以动态地给对象添加属性,读取的值和返回的值都可以通过getter、setter自己定义,所以我们获取、修改值时,直接去操作data对象。

// init.js function initData(vm) { const opt = vm.$option let data = opt.data if (typeof data === 'function') { data = data() } // 在vm上定义_data和data中的数据 defineGlobal(vm, '_data', { _data: data }) Object.keys(data).forEach(key => { defineGlobal(vm, key, data) }) } /** * 在vm上定义代理对象 * @param {*} vm vue实例 * @param {*} key 需要在实例上定义的key * @param {*} source 需要被代理的源对象 */ function defineGlobal(vm, key, source) { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get() { return source[key] }, set(v) { source[key] = v } }) } 四、对象监听

与vue挂载data类似,可以使用Object.defineProperty去代理data中的所有属性,并且递归地去代理所有的子属性,通过setter就可以监听属性的修改。

具体实现:创建一个Observer类,需要监听的对象作为参数传入构造函数,然后在这个类的构造函数中去给对象添加响应式。添加响应式监听之后,把类实例对象挂到监听对象中去,作为一个已经被监听的标志位,也直接使用监听的相关方法,否则,其实直接通过方法处理就足够了,并不需要作为一个类。

// init.js function initData(vm) {     ... // 观察data中内容 observe(data)     ... } // observe/index.js class Observer { constructor(obj) { if (Array.isArray(obj)) { ... } else { this.observeObj(obj) } // 在被监听的对象上,定义已被监听的标志位,指向Observer对象自身 // 在其它文件中,也方便直接使用监听的相关方法 Object.defineProperty(obj, '__ob__', { value: this }) } /** * 监听对象 * @param {Object} obj 需要观察的对象 */ observeObj(obj) { Object.keys(obj).forEach(key => { const val = obj[key] // 尝试监听对象的属性 observe(val) // 定义响应式 defineReactive(obj, key) }) } } // 导出的方法 /** * * @param {Object} obj 需要监听的对象 * @returns */ export function observe(obj) { if (typeof obj !== 'object') { return null } if (obj.__ob__) { return obj.__ob__ } return new Observer(obj) } /** * 给对象添加响应式 * @param {Object} obj 需要定义响应式的对象 * @param {String} key 对象的属性key */ export function defineReactive(obj, key) { let value = obj[key] Object.defineProperty(obj, key, { configurable: true, enumerable: true, get() { return value }, set(val) { console.log('set data', obj, key, val); value = val } }) } 五、数组监听

由于数组长度可能比较大,如果对数组的每个对象都进行响应式监听,则性能损耗太高,比如数组长度为1万,则需要对一万个属性的进行响应式监听:Object.defineProperty(arr, i, {})

由于数组对数组进行操作,主要时通过push、pop、splice等等方法去修改的,通过下标直接修改比较少,因此只监听数组的方法调用进行监听。

具体实现:

举例,我们要监听对象arr的push方法,不能直接重写原型对象方法arr.__proto__.push = XXX,因为arr._proto指向的是Array.prototype, 直接修改后,所有数组对象的push方法都会被修改。

利用原型链中会逐级查找属性的特点,我们可以创建一个对象,在这个对象中重新定义push等方法,然后,需要监听的数组,只需要原型对象指我们创建的数组原型重写对象,就可以实现监听

而这个对象的原型对象应该指向Array对象,因为我们并不需要重写所有的数组方法,而是只需要重写一部分会修改原数组的方法,并且,我们重新定义的方法,最后也是要调用Array中的原方法是实现方法逻辑的,相当于对方法进行了一层代理,每次调用时,能够通知到我们,做一些操作。

// array.js // 需要监听的方法 const observeMethods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] const arrayProto = Array.prototype // 创建一个空对象,原型对象指向Array的原型属性上 const obArrayProto = Object.create(arrayProto) // 重写需要监听的方法 observeMethods.forEach(methodName => { const originalMethod = arrayProto[methodName] obArrayProto[methodName] = function (...args) { let inserted // 新增元素的方法,需要给新元素添加响应式 switch (methodName) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break default: break; } const observe = this.__ob__ if (inserted) { observe.observeArray(inserted) } console.log(methodName, args); // 需要使用call,因为originalMethod方法没有通过XXX对象去调用,因此,如果直接执行,方法中的this会指向window,在严格模式下会指向undefined // 通过call方法调用,让originalMethod方法内的this指向数组对象,因为这个方法是通过arr.XXX()调用的 return originalMethod.call(this, ...args) } }) /** * 监听数组方法 * @param {Array} arr */ export function observeArrayMethod(arr) { // 数组的原型对象指向重写了部分方法的新的原型对象 Object.setPrototypeOf(arr, obArrayProto) } // ********************** observe/index.js ******************** import { observeArrayMethod } from "./array" class Observer { constructor(obj) { if (Array.isArray(obj)) { this.observeArray(obj) } else { ... }         ... } /** * 监听数组 * @param {Array} arr 需要观察的数组 */ observeArray(arr) { // 监听数组的修改方法 observeArrayMethod(arr) // 尝试监听数组内的所有元素 arr.forEach(observe) } } 总结: 目录结构:

gitee源码地址:

https://gitee.com/ZepngLin/my-vue/tree/%EF%BC%88%E4%BA%8C%EF%BC%89%E5%93%8D%E5%BA%94%E5%BC%8F%E5%AE%9E%E7%8E%B0



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3